Segmentacja klientów kart kredytowych za pomocą analizy skupień¶

Michał Lata (s223352), Semion Lisichik (s217519), Kamil Kluska (s223388), Igor Kucharski (s223535), Stanisław Mierzejewski (s223558)

Streszczenie¶

Celem niniejszego projektu było przeprowadzenie segmentacji klientów kart kredytowych na podstawie ich cech demograficznych oraz zachowań finansowych, z wykorzystaniem analizy skupień. Analiza została przeprowadzona na rzeczywistym zbiorze danych zawierającym informacje o użytkownikach kart, obejmującym m.in. wiek, płeć, limit kredytowy, częstotliwość korzystania z karty, saldo zadłużenia oraz historię transakcji.

Prace rozpoczęto od oczyszczenia i przygotowania danych: usunięto zmienne nieistotne, ujednolicono typy danych, przeprowadzono enkodowanie zmiennych kategorycznych oraz standaryzację cech numerycznych. W celu ułatwienia wizualizacji i redukcji wymiarowości danych wykorzystano metodę analizy głównych składowych (PCA), która pozwoliła ograniczyć liczbę zmiennych wejściowych przy zachowaniu 33% wariancji. Następnie dane otrzymane w wyniku PCA zostały użyte do stworzenia klastrów.

Do właściwej segmentacji zastosowano algorytm KMeans, dobierając optymalną liczbę klastrów na podstawie wykresu „łokcia” oraz współczynnika sylwetki. Ostatecznie wyróżniono trzy wyraźne segmenty klientów, różniące się między sobą poziomem aktywności finansowej, wartością kredytową oraz zaangażowaniem w korzystanie z usług banku. Dodatkowo, zastosowano hierarchiczny algorytm Warda, który umożliwił stworzenie dendrogramu i wizualizację struktury klastrów w postaci hierarchicznej, co pozwoliło na głębszą analizę zależności między grupami.

Uzyskane wyniki umożliwiają stworzenie profili klientów, co może zostać wykorzystane w praktyce m.in. do projektowania spersonalizowanych ofert marketingowych, zarządzania ryzykiem kredytowym oraz identyfikacji klientów zagrożonych odejściem. Projekt pokazuje, jak techniki eksploracji danych mogą wspierać decyzje biznesowe w sektorze finansowym i stanowi punkt wyjścia do dalszych, bardziej zaawansowanych analiz predykcyjnych.

Słowa kluczowe:¶

Klasteryzacja, KMeans, PCA, Algorytm Hierarchiczny Warda, Segmentacja klientów, Analiza danych, Churn

Słowniczek:¶

  1. Klasteryzacja - technika analizy danych polegająca na grupowaniu obiektów w takie zbiory (klastry), w których obiekty wewnatrz klastra są do siebie bardziej podobne niż do obiektów z innych klastrów. W tym projekcie zastosowano klasteryzację w celu podziału klientów kart kredytowych na grupy o podobnych zachowaniach finansowych.
  2. KMeans - algorytm klasteryzacji oparty na podziale danych na k grup. Celem algorytmu jest minimalizacja sumy kwadratów odległości od punktów do środków klastra.
  3. PCA (Principal Component Analysis) - to metoda redukcji wymiarowości, która pozwala na uproszczenie dużych zbiorów danych poprzez przekształcenie ich do nowego układu współrzędnych, gdzie pierwsze składowe (wymiary) mają największą wariancję.
  4. Algorytm hierarchiczny Warda – Metoda klasteryzacji, która łączy obiekty w hierarchiczny sposób, minimalizując różnice w obrębie grup. Jest to metoda aglomeracyjna, która znajduje zastosowanie przy tworzeniu drzew hierarchicznych (dendrogramów).
  5. Segmentacja klientów - proces dzielenia populacji klientów na mniejsze grupy (segmenty), które mają podobne cechy.
  6. Analiza danych - Proces badania, oczyszczania, przekształcania i modelowania danych w celu wyciągania z nich użytecznych informacji, wniosków i wspierania podejmowania decyzji.
  7. Churn - Termin stosowany w marketingu, który oznacza utratę klientów lub rezygnację z usługi. W kontekście analizy danych o klientach kart kredytowych, churn oznaczaa klientów, którzy zdecydowali się zakończyć korzystanie z usług banku.

Wprowadzenie¶

Rynek kart kredytowych od dekad odgrywa istotną rolę w systemie finansowym, stanowiąc jeden z najpopularniejszych instrumentów płatniczych na świecie. Jego rozwój był ściśle powiązany z postępującą globalizacją usług finansowych, wzrostem konsumpcji oraz potrzebą zapewnienia konsumentom szybkiego i wygodnego dostępu do środków finansowych. Pierwsze karty kredytowe, jakie znamy dziś, pojawiły się w Stanach Zjednoczonych w latach 50. XX wieku, a ich gwałtowna popularyzacja nastąpiła w kolejnych dekadach, wraz z rozwojem handlu elektronicznego oraz cyfryzacją usług bankowych.

Współczesny rynek kart kredytowych to jednak nie tylko narzędzie płatnicze – to także źródło ogromnych ilości danych o zachowaniach konsumenckich. Informacje te, odpowiednio przetwarzane i analizowane, pozwalają instytucjom finansowym podejmować trafniejsze decyzje strategiczne: od oceny ryzyka kredytowego, przez personalizację oferty, aż po przeciwdziałanie odpływowi klientów (tzw. churn). W dobie dynamicznych zmian w sektorze finansowym – napędzanych m.in. przez rozwój fintechów, zmieniające się oczekiwania klientów oraz rosnącą presję na cyfryzację – umiejętność wykorzystania danych staje się kluczową przewagą konkurencyjną.

Jednym z podejść, które umożliwiają lepsze zrozumienie bazy klientów, jest segmentacja – czyli podział użytkowników na grupy o podobnych cechach lub zachowaniach. W przeciwieństwie do klasycznych metod segmentacji opartych na kryteriach demograficznych, analiza danych umożliwia tworzenie segmentów w oparciu o rzeczywiste wzorce aktywności klientów. Techniki takie jak analiza skupień (klasteryzacja) znajdują szerokie zastosowanie w marketingu, CRM (Customer Relationship Management) oraz w modelach predykcyjnych.

W niniejszym projekcie podjęto próbę segmentacji klientów kart kredytowych na podstawie dostępnych danych zawierających informacje demograficzne i behawioralne. Głównym celem analizy jest wyodrębnienie grup klientów o podobnych profilach, co może w przyszłości pomóc w personalizacji ofert, optymalizacji kampanii marketingowych oraz w strategiach utrzymania lojalnych użytkowników.

Proces analityczny rozpoczęto od wstępnego przetwarzania danych – usunięcia nieistotnych cech, konwersji zmiennych kategorycznych oraz ich normalizacji. W celu uproszczenia struktury danych i lepszego zobrazowania relacji między obserwacjami, zastosowano metodę głównych składowych (PCA) do redukcji wymiarowości. Następnie wykorzystano popularny algorytm klasteryzacji – KMeans – w celu grupowania klientów w klastry o zbliżonym profilu.

Wyniki analizy pokazują, że możliwe jest wyodrębnienie kilku znacząco różniących się grup użytkowników, m.in. klientów bardzo aktywnych, umiarkowanych oraz mało zaangażowanych. Takie podejście umożliwia nie tylko bardziej precyzyjne zarządzanie relacją z klientem, ale także wspiera identyfikację klientów o wysokim ryzyku odejścia lub tych o największym potencjale wzrostu.

Cel i zakres badania:¶

Celem badania była segmentacja klientów kart kredytowych w celu wyróżnienia grup o podobnych cechach zachowań finansowych i demograficznych. W ramach analizy:

  • Zastosowano metodę PCA do redukcji wymiarowości danych,
  • Użyto algorytmu KMeans do podziału klientów na klastery,
  • Zastosowano hierarchiczny algorytm Warda do stworzenia dendrogramu i analizy struktury klastrów w postaci hierarchicznej, co umożliwiło głębsze zrozumienie zależności między grupami,
  • Określono, które zmienne mają największy wpływ na podział klientów,
  • Zbadano, jak różne segmenty klientów mogą wpływać na strategie bankowe.

Przegląd literatury¶

W swojej pracy [4] Bakoben, Bellotti i Adams wykorzystali analizę skupień do segmentacji klientów kart kredytowych na podstawie ich zachowań finansowych. Celem było zidentyfikowanie grup klientów o różnym poziomie ryzyka kredytowego poprzez analizę parametrów kont. Mancisidor, Kampffmeyer, Aas i Jenssen w swojej pracy [5] zaproponowali wykorzystanie autoenkoderów wariacyjnych (VAE) do segmentacji klientów na podstawie ukrytych klastrów. Podejście to pozwala na identyfikację segmentów klientów z różnymi profilami ryzyka kredytowego, co może wspierać procesy oceny zdolności kredytowej oraz działania marketingowe. Maciejewski natomiast w swoim artykule [3] przedstawił zastosowanie hierarchicznej analizy skupień (metoda Warda) do segmentacji użytkowników bankowości elektronicznej. Na podstawie badań przeprowadzonych na próbie 1075 osób wyodrębniono trzy segmenty: młodych entuzjastów, ostrożnych pragmatyków oraz nieufnych i nieświadomych użytkowników.

Zmienne wybrane do analizy - wraz z opisami¶

  1. Attrition_Flag - Określa, czy klient jest aktywny, czy odszedł od banku : ('Existing Customer' jesli klient jest aktywny, 'Attrited Customer' jesli klient odszedł)
  2. Customer_Age - Wiek klienta w latach
  3. Gender - Płeć klienta : ('M' jesli mężczyzna, 'F' jesli kobieta)
  4. Dependent_count - Liczba osób na utrzymaniu klienta
  5. Education_Level - Poziom wykształcenia klienta : ('College', 'Doctorate', 'Graduate', 'High School', 'Post-Graduate', 'Uneducated', 'Unknown')
  6. Marital_Status - Stan cywilny klienta : ('Divorced', 'Married', 'Single', 'Unknown')
  7. Income_Category - Przedział dochodowy klienta : ('Unknown', 'Less than $40K', '$40K - $60K', '$60K - $80K', '$80K - $120K', '$120K +')
  8. Card_Category - Typ karty kredytowej klienta : ('Blue', 'Gold', 'Platinum', 'Silver')
  9. Months_on_book - Liczba miesięcy, przez które klient jest związany z bankiem
  10. Total_Relationship_Count - Całkowita liczba produktów bankowych, z których korzysta klient :
  11. Months_Inactive_12_mon - Liczba miesięcy, w których klient był nieaktywny w ciągu ostatnich 12 miesięcy : (numeryczna)
  12. Contacts_Count_12_mon - Liczba kontaktów z bankiem w ciągu ostatnich 12 miesięcy
  13. Credit_Limit - Limit kredytowy na karcie klienta
  14. Total_Revolving_Bal - Całkowite saldo odnawialne na karcie kredytowej
  15. Avg_Open_To_Buy - Otwarta linia kredytowa na zakup (średnia z ostatnich 12 miesięcy)
  16. Total_Amt_Chng_Q4_Q1 - Zmiana w wydatkach na karcie między czwartym a pierwszym kwartałem
  17. Total_Trans_Amt - Całkowita kwota transakcji dokonanych przez klienta (ostatnie 12 miesięcy)
  18. Total_Trans_Ct - Całkowita liczba transakcji dokonanych przez klienta (ostatnie 12 miesięcy)
  19. Total_Ct_Chng_Q4_Q1 - Zmiana w liczbie transakcji między czwartym a pierwszym kwartałem
  20. Avg_Utilization_Ratio - Średni współczynnik wykorzystania karty (od 0 do 1)

Cel i zakres badania:¶

Celem badania była segmentacja klientów kart kredytowych w celu wyróżnienia grup o podobnych cechach zachowań finansowych i demograficznych. W ramach analizy:

  • Zastosowano metodę PCA do redukcji wymiarowości danych,
  • Użyto algorytmu KMeans do podziału klientów na klastery,
  • Zastosowano hierarchiczny algorytm Warda do stworzenia dendrogramu i analizy struktury klastrów w postaci hierarchicznej, co umożliwiło głębsze zrozumienie zależności między grupami,
  • Określono, które zmienne mają największy wpływ na podział klientów,
  • Zbadano, jak różne segmenty klientów mogą wpływać na strategie bankowe.

Import potrzebnych bibliotek do analizy danych¶

In [ ]:
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns

Wczytanie ramki danych¶

In [ ]:
df = pd.read_csv("BankChurners.csv")

Usuwamy niepotrzebne kolumny - pierwszą kolumnę z numerami klientów, która nie jest istotna oraz dwie ostatnie według instrukcji załączanej do ramki danych

In [ ]:
df = df.drop(["CLIENTNUM",
              "Naive_Bayes_Classifier_Attrition_Flag_Card_Category_Contacts_Count_12_mon_Dependent_count_Education_Level_Months_Inactive_12_mon_1", 
             "Naive_Bayes_Classifier_Attrition_Flag_Card_Category_Contacts_Count_12_mon_Dependent_count_Education_Level_Months_Inactive_12_mon_2" ], 
             axis=1)

Podział na zmienne kategoryczne i numeryczne¶

In [ ]:
numerical_features = list(df.dtypes[(df.dtypes != 'object') & (df.dtypes != 'category')].index)
categorical_features = list(df.dtypes[(df.dtypes == 'object') | (df.dtypes == 'category')].index) 

Obliczenie średniej, odchylenia standardowego, minimum, maksimum i wszystkich kwartyli w tym mediany¶

In [ ]:
df.describe().transpose()
Out[ ]:
count mean std min 25% 50% 75% max
Customer_Age 10127.0 46.325960 8.016814 26.0 41.000 46.000 52.000 73.000
Dependent_count 10127.0 2.346203 1.298908 0.0 1.000 2.000 3.000 5.000
Months_on_book 10127.0 35.928409 7.986416 13.0 31.000 36.000 40.000 56.000
Total_Relationship_Count 10127.0 3.812580 1.554408 1.0 3.000 4.000 5.000 6.000
Months_Inactive_12_mon 10127.0 2.341167 1.010622 0.0 2.000 2.000 3.000 6.000
Contacts_Count_12_mon 10127.0 2.455317 1.106225 0.0 2.000 2.000 3.000 6.000
Credit_Limit 10127.0 8631.953698 9088.776650 1438.3 2555.000 4549.000 11067.500 34516.000
Total_Revolving_Bal 10127.0 1162.814061 814.987335 0.0 359.000 1276.000 1784.000 2517.000
Avg_Open_To_Buy 10127.0 7469.139637 9090.685324 3.0 1324.500 3474.000 9859.000 34516.000
Total_Amt_Chng_Q4_Q1 10127.0 0.759941 0.219207 0.0 0.631 0.736 0.859 3.397
Total_Trans_Amt 10127.0 4404.086304 3397.129254 510.0 2155.500 3899.000 4741.000 18484.000
Total_Trans_Ct 10127.0 64.858695 23.472570 10.0 45.000 67.000 81.000 139.000
Total_Ct_Chng_Q4_Q1 10127.0 0.712222 0.238086 0.0 0.582 0.702 0.818 3.714
Avg_Utilization_Ratio 10127.0 0.274894 0.275691 0.0 0.023 0.176 0.503 0.999

Obliczenie skośności¶

In [ ]:
df[numerical_features].skew()
Out[ ]:
Customer_Age               -0.033605
Dependent_count            -0.020826
Months_on_book             -0.106565
Total_Relationship_Count   -0.162452
Months_Inactive_12_mon      0.633061
Contacts_Count_12_mon       0.011006
Credit_Limit                1.666726
Total_Revolving_Bal        -0.148837
Avg_Open_To_Buy             1.661697
Total_Amt_Chng_Q4_Q1        1.732063
Total_Trans_Amt             2.041003
Total_Trans_Ct              0.153673
Total_Ct_Chng_Q4_Q1         2.064031
Avg_Utilization_Ratio       0.718008
dtype: float64

Wyświetlenie typów kolumn¶

In [ ]:
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 10127 entries, 0 to 10126
Data columns (total 20 columns):
 #   Column                    Non-Null Count  Dtype  
---  ------                    --------------  -----  
 0   Attrition_Flag            10127 non-null  object 
 1   Customer_Age              10127 non-null  int64  
 2   Gender                    10127 non-null  object 
 3   Dependent_count           10127 non-null  int64  
 4   Education_Level           10127 non-null  object 
 5   Marital_Status            10127 non-null  object 
 6   Income_Category           10127 non-null  object 
 7   Card_Category             10127 non-null  object 
 8   Months_on_book            10127 non-null  int64  
 9   Total_Relationship_Count  10127 non-null  int64  
 10  Months_Inactive_12_mon    10127 non-null  int64  
 11  Contacts_Count_12_mon     10127 non-null  int64  
 12  Credit_Limit              10127 non-null  float64
 13  Total_Revolving_Bal       10127 non-null  int64  
 14  Avg_Open_To_Buy           10127 non-null  float64
 15  Total_Amt_Chng_Q4_Q1      10127 non-null  float64
 16  Total_Trans_Amt           10127 non-null  int64  
 17  Total_Trans_Ct            10127 non-null  int64  
 18  Total_Ct_Chng_Q4_Q1       10127 non-null  float64
 19  Avg_Utilization_Ratio     10127 non-null  float64
dtypes: float64(5), int64(9), object(6)
memory usage: 1.5+ MB
In [ ]:
df.isnull().sum()>0
Out[ ]:
Attrition_Flag              False
Customer_Age                False
Gender                      False
Dependent_count             False
Education_Level             False
Marital_Status              False
Income_Category             False
Card_Category               False
Months_on_book              False
Total_Relationship_Count    False
Months_Inactive_12_mon      False
Contacts_Count_12_mon       False
Credit_Limit                False
Total_Revolving_Bal         False
Avg_Open_To_Buy             False
Total_Amt_Chng_Q4_Q1        False
Total_Trans_Amt             False
Total_Trans_Ct              False
Total_Ct_Chng_Q4_Q1         False
Avg_Utilization_Ratio       False
dtype: bool

Obsłużenie braków danych¶

Powyżej widać, że nie mamy braków danych, zatem nie musimy ich obsługiwać.

Wykresy przed obsłużeniem outliers¶

Histogramy¶

In [ ]:
small_percent = ['Dependent_count', 'Total_Relationship_Count', 'Months_Inactive_12_mon', 'Contacts_Count_12_mon']
df.drop(small_percent, axis=1).hist(bins=16, figsize=(15, 6), layout=(3, 5))
plt.tight_layout()
plt.show()
No description has been provided for this image

Wykresy słupkowe¶

In [ ]:
fig, axs = plt.subplots(1, 4, figsize=(16, 4))

for i, col in enumerate(small_percent):
    percent_distribution = (df[col].value_counts(normalize=True) * 100).sort_index()
    axs[i].bar(percent_distribution.index, percent_distribution.values)
    axs[i].set_xlabel(col)
    axs[i].set_ylabel("Procent")
    axs[i].grid(axis='y', linestyle='--', alpha=0.7)

plt.tight_layout()
plt.show()
No description has been provided for this image
In [ ]:
fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(16, 12))
for ax, col in zip(axes.flatten(), categorical_features):
    percent = (df[col].value_counts()/len(df[col])*100).round(2)
    sns.barplot(x=percent.index, y=percent, ax=ax, hue=percent.index)
    ax.set_ylabel('Procent')
    ax.set_xticklabels(ax.get_xticklabels(), rotation=90)
    for p in ax.patches:
        ax.annotate(f'{p.get_height():.2f}', 
                    (p.get_x() + p.get_width() / 2., p.get_height()), 
                    ha='center', va='bottom')
fig.subplots_adjust(wspace=0.3, hspace=0.7)
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\90724676.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90)
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\90724676.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90)
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\90724676.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90)
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\90724676.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90)
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\90724676.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90)
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\90724676.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90)
No description has been provided for this image

Wykresy pudełkowe - przed obsłużeniem outliers¶

In [ ]:
df.plot.box(subplots=True, layout=(5, 3), figsize=(15, 15))
plt.tight_layout()
plt.show()
No description has been provided for this image

Obsługa outliers¶

Obliczamy procent outliers dla każdej kolumny¶

In [ ]:
iqr = df[numerical_features].quantile(0.75) - df[numerical_features].quantile(0.25)
was_1 = df[numerical_features].quantile(0.25) - 1.5 * iqr
was_2 = df[numerical_features].quantile(0.75) + 1.5 * iqr
iqr
Out[ ]:
Customer_Age                  11.000
Dependent_count                2.000
Months_on_book                 9.000
Total_Relationship_Count       2.000
Months_Inactive_12_mon         1.000
Contacts_Count_12_mon          1.000
Credit_Limit                8512.500
Total_Revolving_Bal         1425.000
Avg_Open_To_Buy             8534.500
Total_Amt_Chng_Q4_Q1           0.228
Total_Trans_Amt             2585.500
Total_Trans_Ct                36.000
Total_Ct_Chng_Q4_Q1            0.236
Avg_Utilization_Ratio          0.480
dtype: float64
In [ ]:
outliers_per_column = ((df[numerical_features] < was_1) | (df[numerical_features] > was_2)).sum()
outliers_percentage = (outliers_per_column / len(df)) * 100
print("\nProcent outlierów w kolumnach:")
print(outliers_percentage)
Procent outlierów w kolumnach:
Customer_Age                0.019749
Dependent_count             0.000000
Months_on_book              3.811593
Total_Relationship_Count    0.000000
Months_Inactive_12_mon      3.268490
Contacts_Count_12_mon       6.211119
Credit_Limit                9.716599
Total_Revolving_Bal         0.000000
Avg_Open_To_Buy             9.509233
Total_Amt_Chng_Q4_Q1        3.910339
Total_Trans_Amt             8.847635
Total_Trans_Ct              0.019749
Total_Ct_Chng_Q4_Q1         3.890590
Avg_Utilization_Ratio       0.000000
dtype: float64

Dla outliers w kolumnach w których ich procent jest mniejszy niż 5 usuwamy je, z kolei tam gdzie ich procent jest większy równy niż 5 zastępujemy ich wartości wąsami.

In [ ]:
low_cols = outliers_percentage[outliers_percentage < 5].index.tolist() 
high_cols = outliers_percentage[outliers_percentage >= 5].index.tolist()
In [ ]:
df = df[~((df[low_cols] < was_1[low_cols]) | (df[low_cols] > was_2[low_cols])).any(axis = 1)]
In [ ]:
df.loc[:, high_cols] = df[high_cols].clip(
        lower=was_1[high_cols],
        upper=was_2[high_cols],
        axis=1
    )
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\1237488139.py:1: FutureWarning: Setting an item of incompatible dtype is deprecated and will raise in a future error of pandas. Value '[2. 2. 2. ... 4. 3. 4.]' has dtype incompatible with int64, please explicitly cast to a compatible dtype first.
  df.loc[:, high_cols] = df[high_cols].clip(
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\1237488139.py:1: FutureWarning: Setting an item of incompatible dtype is deprecated and will raise in a future error of pandas. Value '[1201.   1570.   1207.   ... 8619.25 8395.   8619.25]' has dtype incompatible with int64, please explicitly cast to a compatible dtype first.
  df.loc[:, high_cols] = df[high_cols].clip(

Po obsłużeniu outliers¶

Wykresy pudełkowe¶

In [ ]:
df.plot.box(subplots=True, layout=(5, 3), figsize=(15, 15))
plt.tight_layout()
plt.show()
No description has been provided for this image

Histogramy¶

In [ ]:
df.drop(['Dependent_count', 'Total_Relationship_Count', 'Months_Inactive_12_mon', 'Contacts_Count_12_mon'], axis=1).hist(bins=16, figsize=(15, 6), layout=(3, 5))
plt.tight_layout()
plt.show()
No description has been provided for this image

Wykresy słupkowe¶

In [ ]:
fig, axs = plt.subplots(1, 4, figsize=(16, 4))

for i, col in enumerate(small_percent):
    percent_distribution = (df[col].value_counts(normalize=True) * 100).sort_index()
    axs[i].bar(percent_distribution.index, percent_distribution.values)
    axs[i].set_xlabel(col)
    axs[i].set_ylabel("Procent")
    axs[i].grid(axis='y', linestyle='--', alpha=0.7)


plt.tight_layout()
plt.show()
No description has been provided for this image

Obliczenie korelacji¶

In [ ]:
df[numerical_features].corr()
Out[ ]:
Customer_Age Dependent_count Months_on_book Total_Relationship_Count Months_Inactive_12_mon Contacts_Count_12_mon Credit_Limit Total_Revolving_Bal Avg_Open_To_Buy Total_Amt_Chng_Q4_Q1 Total_Trans_Amt Total_Trans_Ct Total_Ct_Chng_Q4_Q1 Avg_Utilization_Ratio
Customer_Age 1.000000 -0.116512 0.746226 -0.009817 0.040425 -0.010358 0.001680 0.020414 -0.000647 -0.078913 -0.034566 -0.060922 -0.045330 0.018804
Dependent_count -0.116512 1.000000 -0.103328 -0.035611 -0.018234 -0.029568 0.060118 -0.000713 0.060336 -0.023812 0.034102 0.031549 0.001550 -0.032719
Months_on_book 0.746226 -0.103328 1.000000 -0.009697 0.054766 -0.002067 0.006452 0.012822 0.004718 -0.054043 -0.023737 -0.039594 -0.035614 0.004314
Total_Relationship_Count -0.009817 -0.035611 -0.009697 1.000000 0.000221 0.074364 -0.061464 0.001799 -0.062175 0.008415 -0.366008 -0.249780 0.006603 0.058121
Months_Inactive_12_mon 0.040425 -0.018234 0.054766 0.000221 1.000000 0.041486 -0.020587 -0.052920 -0.015413 -0.010094 -0.058666 -0.083355 -0.048004 -0.019011
Contacts_Count_12_mon -0.010358 -0.029568 -0.002067 0.074364 0.041486 1.000000 0.016318 -0.053271 0.021637 -0.024042 -0.157194 -0.167681 -0.102249 -0.052915
Credit_Limit 0.001680 0.060118 0.006452 -0.061464 -0.020587 0.016318 1.000000 0.053896 0.994376 0.009957 0.147594 0.071717 -0.017888 -0.515554
Total_Revolving_Bal 0.020414 -0.000713 0.012822 0.001799 -0.052920 -0.053271 0.053896 1.000000 -0.046915 0.019757 0.067674 0.074571 0.076437 0.621056
Avg_Open_To_Buy -0.000647 0.060336 0.004718 -0.062175 -0.015413 0.021637 0.994376 -0.046915 1.000000 0.007757 0.142089 0.065296 -0.025783 -0.584075
Total_Amt_Chng_Q4_Q1 -0.078913 -0.023812 -0.054043 0.008415 -0.010094 -0.024042 0.009957 0.019757 0.007757 1.000000 0.178025 0.141140 0.268157 0.013656
Total_Trans_Amt -0.034566 0.034102 -0.023737 -0.366008 -0.058666 -0.157194 0.147594 0.067674 0.142089 0.178025 1.000000 0.855930 0.245666 -0.049074
Total_Trans_Ct -0.060922 0.031549 -0.039594 -0.249780 -0.083355 -0.167681 0.071717 0.074571 0.065296 0.141140 0.855930 1.000000 0.299843 0.016773
Total_Ct_Chng_Q4_Q1 -0.045330 0.001550 -0.035614 0.006603 -0.048004 -0.102249 -0.017888 0.076437 -0.025783 0.268157 0.245666 0.299843 1.000000 0.086487
Avg_Utilization_Ratio 0.018804 -0.032719 0.004314 0.058121 -0.019011 -0.052915 -0.515554 0.621056 -0.584075 0.013656 -0.049074 0.016773 0.086487 1.000000

Generacja heat mapy korelacji¶

In [ ]:
corr = df[numerical_features].corr()

plt.figure(figsize=(12, 10))
sns.heatmap(
    corr,
    annot=True,
    fmt=".2f",  
    cmap='viridis',  
    linewidths=.5,
    cbar_kws={"shrink": .8}
)
plt.xticks(rotation=90)  
plt.yticks(rotation=0)   
plt.title("Heatmapa korelacji", pad=20)
plt.show()
No description has been provided for this image
In [ ]:
sns.pairplot(df)
Out[ ]:
<seaborn.axisgrid.PairGrid at 0x288a611de80>
No description has been provided for this image

Analiza skupień¶

In [ ]:
df.sample(5)
Out[ ]:
Attrition_Flag Customer_Age Gender Dependent_count Education_Level Marital_Status Income_Category Card_Category Months_on_book Total_Relationship_Count Months_Inactive_12_mon Contacts_Count_12_mon Credit_Limit Total_Revolving_Bal Avg_Open_To_Buy Total_Amt_Chng_Q4_Q1 Total_Trans_Amt Total_Trans_Ct Total_Ct_Chng_Q4_Q1 Avg_Utilization_Ratio
7779 Existing Customer 53 M 4 College Single $60K - $80K Blue 42 3 3 3.0 3096.00 0 3096.00 0.684 4709.0 87 0.776 0.000
3693 Attrited Customer 38 F 2 Graduate Single Unknown Blue 24 3 2 3.0 2214.00 570 1644.00 0.688 2066.0 45 0.250 0.257
7728 Existing Customer 57 F 3 High School Single Unknown Blue 37 3 1 2.0 3052.00 1603 1449.00 0.776 4210.0 82 0.907 0.525
1815 Existing Customer 38 M 2 Graduate Married $80K - $120K Blue 31 4 1 2.0 23836.25 0 22660.75 0.781 1886.0 48 0.500 0.000
6708 Attrited Customer 39 F 2 High School Single Unknown Blue 34 3 3 1.0 2069.00 550 1519.00 0.783 2716.0 43 0.433 0.266

Zmienne wybrane do analizy¶

Na podstawie heatmapy reprezentującej korelacje pomiędzy każdą zmienną zostały wybrane następujące zmienne:

In [ ]:
useful_features = [
    "Customer_Age",
    #'Gender',
    #'Education_Level',
    #"Dependent_count",
    "Income_Category",
    #"Card_Category",
    #"Months_on_book",
    "Total_Relationship_Count",
    "Months_Inactive_12_mon",
    "Contacts_Count_12_mon",
    "Credit_Limit",
    "Total_Revolving_Bal",
    #"Avg_Open_To_Buy",
    "Total_Amt_Chng_Q4_Q1",
    "Total_Trans_Amt",
    #"Total_Trans_Ct",
    "Total_Ct_Chng_Q4_Q1",
    "Avg_Utilization_Ratio"
]
In [ ]:
plt.figure(figsize=(12, 10))
sns.heatmap(df[useful_features].corr(numeric_only=True), cmap='viridis', annot=True)
Out[ ]:
<Axes: >
No description has been provided for this image
In [ ]:
X = pd.get_dummies(df[useful_features])
In [ ]:
X.sample(5)
Out[ ]:
Customer_Age Total_Relationship_Count Months_Inactive_12_mon Contacts_Count_12_mon Credit_Limit Total_Revolving_Bal Total_Amt_Chng_Q4_Q1 Total_Trans_Amt Total_Ct_Chng_Q4_Q1 Avg_Utilization_Ratio Income_Category_$120K + Income_Category_$40K - $60K Income_Category_$60K - $80K Income_Category_$80K - $120K Income_Category_Less than $40K Income_Category_Unknown
8744 43 4 2 2.0 11362.0 1871 0.859 7849.0 0.630 0.165 False False False False True False
5758 52 3 3 2.0 13090.0 0 0.750 3851.0 1.000 0.000 False True False False False False
8482 50 2 2 2.0 4060.0 2336 0.943 5173.0 0.706 0.575 False False False False True False
2304 41 4 2 2.0 2624.0 1319 0.768 4189.0 0.609 0.503 False False True False False False
4144 43 1 3 4.0 4579.0 1506 0.443 1915.0 0.375 0.329 False False False False True False
In [ ]:
X.shape
Out[ ]:
(8802, 16)
In [ ]:
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
from sklearn.decomposition import PCA

Standaryzacja danych¶

Przed zastosowaniem algorytmu KMeans oraz PCA niezbędna jest standaryzacja danych, ponieważ oba algorytmy są wrażliwe na skalę zmiennych. Dane standaryzuje się najczęściej za pomocą transformacji z-score:

$$ z_i = \frac{x_i - \bar{x}}{\sigma} $$

Gdzie:
Gdzie:

  • $z_i$ – standaryzowana wartość zmiennej,
  • $x_i$ – oryginalna wartość,
  • $\bar{x}$ – średnia arytmetyczna,
  • $\sigma$ – odchylenie standardowe.

In [ ]:
scaler = StandardScaler()
scaled_X = scaler.fit_transform(X)
scaled_X
Out[ ]:
array([[-0.57662619,  0.77665931,  0.85812541, ..., -0.42655886,
        -0.73532078, -0.34721773],
       [ 1.44439732,  0.77665931, -0.30785481, ..., -0.42655886,
         1.35995068, -0.34721773],
       [-0.17242149,  1.41453887, -1.47383502, ..., -0.42655886,
        -0.73532078,  2.88003725],
       ...,
       [-0.30715639,  0.77665931,  0.85812541, ..., -0.42655886,
         1.35995068, -0.34721773],
       [-2.19344499,  0.13877975,  0.85812541, ..., -0.42655886,
        -0.73532078, -0.34721773],
       [-0.44189129,  1.41453887, -0.30785481, ..., -0.42655886,
         1.35995068, -0.34721773]])
In [ ]:
scaled_X.shape
Out[ ]:
(8802, 16)

PCA – analiza głównych składowych¶

PCA (Principal Component Analysis) to technika redukcji wymiarowości, która pozwala przekształcić dane do nowej przestrzeni współrzędnych, tak aby zachować jak najwięcej wariancji oryginalnych danych.

Kluczowe kroki:¶

  1. Obliczenie macierzy kowariancji:
$$ \Sigma = \frac{1}{n-1} X^T X $$
  1. Znalezienie wektorów własnych:
$$ \Sigma v = \lambda v $$
  1. Projekcja danych:
$$ Z = X W $$
In [ ]:
model_pca = PCA(n_components=3)
pc_results = model_pca.fit_transform(scaled_X)
In [ ]:
pc_results.shape
Out[ ]:
(8802, 3)
In [ ]:
model_pca.explained_variance_ratio_
Out[ ]:
array([0.14048076, 0.10468107, 0.0865598 ])

Wariancja wyrażana przez PCA (33%)

In [ ]:
np.sum(model_pca.explained_variance_ratio_)
Out[ ]:
0.3317216265414057

Metoda KMeans¶

Alokacja obiektów niehierarchiczna, która minimalizuje zmienność wewnątrz powstałych skupień, jednocześnie maksymalizując zmienność między skupieniami.
Niech $C_k$ - funkcja, która przyporządkowuje obiektowi numer skupienia.
Niech $C_k^*$ - funkcja realizująca optymalny podział. Wtedy:

$$ C_k^* = \underset{C_k}{\text{argmin}} \sum_{j=1}^{k} \sum_{C_k(i)=j} \rho^2(x_i, \bar{x}_j) $$

gdzie $\rho$ - odległość euklidesowa.


KMeans – klasteryzacja¶

Algorytm KMeans dzieli dane na ( K ) klastrów, tak aby zmniejszyć sumę kwadratów odległości punktów od ich centroidów.

Funkcja kosztu:¶

$$ J = \sum_{i=1}^{K} \sum_{x \in C_i} \| x - \mu_i \|^2 $$

Aktualizacja centroidów:¶

$$ \mu_i = \frac{1}{|C_i|} \sum_{x \in C_i} x $$

Szukanie odpowiedniego k do modelu KMeans

In [ ]:
ssd=[]
silh_scores = []

for k in range(2, 10):
    model = KMeans(n_clusters=k, random_state=1)
    labels = model.fit_predict(pc_results)
    score = silhouette_score(pc_results, labels)
    silh_scores.append(score)

    ssd.append(model.inertia_)

Elbow Method¶

Metoda pomgająca w doborze odpowiedniej liczby klastrów, na wykresie przedstawione są SSD (Sum of Squared Distances, czyli suma kwadratów odległości punktów danych od centroidów (środków) ich przypisanych klastrów) dla każdego k. Aby wybrać odpowiednią liczbę klastrów szukamy tak zwanego łokcia na wykresie.

In [ ]:
plt.figure(figsize=(12, 10))
plt.plot(range(2, 10), ssd, 'o--')
plt.xticks(range(2, 10));
No description has been provided for this image

Różnica pomiędzy środkami

In [ ]:
pd.Series(ssd).diff()
Out[ ]:
0            NaN
1   -7872.374003
2   -4853.737520
3   -2873.753385
4   -1802.146100
5   -1213.822936
6   -1399.813008
7    -907.921430
dtype: float64
In [ ]:
pd.Series(ssd).diff().plot(kind='bar')
Out[ ]:
<Axes: >
No description has been provided for this image

Metoda Silhouette'a¶

Metoda Silhouette'a (sylwetki) służy do oceny jakości klasteryzacji – czyli tego, jak dobrze dane punkty zostały pogrupowane. Dla każdego punktu oblicza się tzw. współczynnik silhouette'a, który określa, jak dobrze punkt pasuje do swojego klastra w porównaniu do innych klastrów.

Krok 1: Obliczenie wartości¶

Dla danego punktu ( i ):

  1. Oblicz średni dystans do wszystkich innych punktów w tym samym klastrze:
$$ a(i) = \frac{1}{|C_i| - 1} \sum_{j \in C_i, j \ne i} d(i, j) $$

gdzie:

  • $C_i$ to klaster, do którego należy punkt $i$,
  • $d(i, j)$ to odległość (np. euklidesowa) między punktami $i$ i $j$,
  • $a(i)$ to średni dystans punktu $i$ do pozostałych punktów w swoim klastrze.
  1. Oblicz średni dystans punktu $i$ do punktów w najbliższym sąsiednim klastrze:
$$ b(i) = \min_{C_k \ne C_i} \left( \frac{1}{|C_k|} \sum_{j \in C_k} d(i, j) \right) $$

Krok 2: Obliczenie współczynnika silhouette'a¶

$$ s(i) = \frac{b(i) - a(i)}{\max\{a(i), b(i)\}} $$
  • $s(i) \in [-1, 1]$
  • Im bliżej 1, tym lepsze przypisanie punktu do klastra
  • Wartości bliskie 0 oznaczają, że punkt leży na granicy między klastrami
  • Wartości ujemne sugerują, że punkt może być przypisany do niewłaściwego klastra

Krok 3: Średni współczynnik silhouette'a¶

Jako miarę jakości całej klasteryzacji można przyjąć średnią wartość $s(i)$ dla wszystkich punktów:

$$ \bar{s} = \frac{1}{n} \sum_{i=1}^{n} s(i) $$

Im większa wartość $\bar{s}$, tym lepsze dopasowanie klastrów do danych.

Sprawdzanie spójności klastrów

In [ ]:
plt.plot(range(2, 10), silh_scores, 'o')
Out[ ]:
[<matplotlib.lines.Line2D at 0x288b6982cf0>]
No description has been provided for this image

Na podstawie Elbow Method i wyniku Silhoutte'a wybieramy K=3

In [ ]:
model = KMeans(n_clusters=3, random_state=1)
labels = model.fit_predict(pc_results)
In [ ]:
X['Kmeans labels']=labels
In [ ]:
X.corr()['Kmeans labels'].sort_values()
Out[ ]:
Credit_Limit                     -0.624496
Total_Revolving_Bal              -0.412681
Income_Category_$80K - $120K     -0.389055
Income_Category_$120K +          -0.287216
Total_Trans_Amt                  -0.229809
Total_Ct_Chng_Q4_Q1              -0.156232
Total_Amt_Chng_Q4_Q1             -0.108352
Income_Category_$60K - $80K      -0.072470
Customer_Age                     -0.033119
Avg_Utilization_Ratio            -0.000366
Income_Category_Unknown           0.001525
Contacts_Count_12_mon             0.085112
Total_Relationship_Count          0.088077
Months_Inactive_12_mon            0.102632
Income_Category_$40K - $60K       0.126002
Income_Category_Less than $40K    0.401966
Kmeans labels                     1.000000
Name: Kmeans labels, dtype: float64

Wykresy klastrów na danych z PCA

In [ ]:
pca_df = pd.DataFrame(data=pc_results, columns=['PC1', 'PC2', 'PC3'])
pca_df.sample(2)
Out[ ]:
PC1 PC2 PC3
1122 1.466871 -0.779399 1.025176
4208 1.859434 0.097030 0.851515
In [ ]:
sns.scatterplot(data=pca_df, x='PC1', y='PC2', hue=labels, palette='viridis')
plt.legend(loc=(1.05, 0.5));
No description has been provided for this image
In [ ]:
sns.scatterplot(data=pca_df, x='PC1', y='PC3', hue=labels, palette='viridis')
plt.legend(loc=(1.05, 0.5));
No description has been provided for this image
In [ ]:
sns.scatterplot(data=pca_df, x='PC2', y='PC3', hue=labels, palette='viridis')
plt.legend(loc=(1.05, 0.5));
No description has been provided for this image

Interpretacja klastrów, KMeans¶

In [ ]:
df['Kmeans k=3']=labels
In [ ]:
plt.figure(figsize=(8, 6))
percent = (df['Kmeans k=3'].value_counts()/len(df['Kmeans k=3'])*100).round(2)
ax = sns.barplot(x=percent.index, y=percent, hue=percent.index)
for p in ax.containers:
    ax.bar_label(p, fmt='%.2f%%', label_type='edge', fontsize=12, padding=3)
plt.ylabel('Procent')
plt.xlabel('Grupa')
Out[ ]:
Text(0.5, 0, 'Grupa')
No description has been provided for this image
In [ ]:
df.corr(numeric_only=True)['Kmeans k=3'].sort_values()
Out[ ]:
Credit_Limit               -0.624496
Avg_Open_To_Buy            -0.578865
Total_Revolving_Bal        -0.412681
Total_Trans_Amt            -0.229809
Total_Trans_Ct             -0.191197
Total_Ct_Chng_Q4_Q1        -0.156232
Total_Amt_Chng_Q4_Q1       -0.108352
Dependent_count            -0.046843
Months_on_book             -0.033707
Customer_Age               -0.033119
Avg_Utilization_Ratio      -0.000366
Contacts_Count_12_mon       0.085112
Total_Relationship_Count    0.088077
Months_Inactive_12_mon      0.102632
Kmeans k=3                  1.000000
Name: Kmeans k=3, dtype: float64
In [ ]:
scaled_df = pd.DataFrame(scaled_X, columns=X.drop('Kmeans labels', axis=1).columns)
scaled_df['Kmeans k=3'] = labels

Wykresy średnich wartości po standaryzacji

In [ ]:
fig, axes = plt.subplots(3, 1, figsize=(8, 16))
for i, ax in zip(range(labels.max()+1), axes):
    scaled_df[scaled_df['Kmeans k=3'] == i].drop('Kmeans k=3', axis=1).mean().plot(kind='barh', title=f'Grupa {i}', ax=ax)
No description has been provided for this image
In [ ]:
fig, axes = plt.subplots(1, 3, figsize=(20, 6))
for i, ax in zip(range(labels.max()+1),axes):
   percent = (df[df['Kmeans k=3']==i]['Income_Category'].value_counts()/len(df[df['Kmeans k=3']==i]['Income_Category'])*100).round(2)
   sns.barplot(x=percent.index, y=percent, ax=ax, hue=percent.index)
   ax.set_title(f'Grupa {i}')
   ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
   ax.set_ylabel('Procent')

   for p in ax.patches:
        ax.annotate(f'{p.get_height():.2f}', 
                    (p.get_x() + p.get_width() / 2., p.get_height()), 
                    ha='center', va='bottom')
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\2432325268.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\2432325268.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\2432325268.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
No description has been provided for this image
In [ ]:
fig, axes = plt.subplots(1, 3, figsize=(20, 6))
for i, ax in zip(range(labels.max()+1),axes):
   percent = (df[df['Kmeans k=3']==i]['Attrition_Flag'].value_counts()/len(df[df['Kmeans k=3']==i]['Attrition_Flag'])*100).round(2)
   sns.barplot(x=percent.index, y=percent, ax=ax, hue=percent.index)
   ax.set_title(f'Grupa {i}')
   ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
   ax.set_ylabel('Procent')
   for p in ax.patches:
        ax.annotate(f'{p.get_height():.2f}', 
                    (p.get_x() + p.get_width() / 2., p.get_height()), 
                    ha='center', va='bottom')
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\3769000308.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\3769000308.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\3769000308.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
No description has been provided for this image
In [ ]:
fig, axes = plt.subplots(1, 3, figsize=(20, 6))
features = ['Total_Revolving_Bal', 'Avg_Utilization_Ratio', 'Total_Trans_Amt']
for f, ax in zip(features, axes):
    sns.barplot(data=df, x='Kmeans k=3', y=f, hue='Card_Category', palette='Set1', ax=ax)
    ax.legend(title='Typ karty',loc=(0.65, 0.76))
    ax.set_xlabel('Grupa');
No description has been provided for this image
In [ ]:
fig, axes = plt.subplots(1, 3, figsize=(20, 6))
for i, ax in zip(range(labels.max()+1),axes):
    sns.barplot(data=df[df['Kmeans k=3'] == i], x='Income_Category', y='Credit_Limit', hue='Income_Category', ax=ax, order=['Unknown', 'Less than $40K', '$40K - $60K', '$60K - $80K',
       '$80K - $120K','$120K +',]);
    ax.set_title(f'Grupa {i}')
    ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\4063709792.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\4063709792.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\4063709792.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
No description has been provided for this image
In [ ]:
fig, axes = plt.subplots(1, 3, figsize=(20, 6))
for i, ax in zip(range(labels.max()+1),axes):
    sns.barplot(data=df[df['Kmeans k=3'] == i], x='Income_Category', y='Total_Revolving_Bal', hue='Income_Category', ax=ax, order=['Unknown', 'Less than $40K', '$40K - $60K', '$60K - $80K',
       '$80K - $120K','$120K +',]);
    ax.set_title(f'Grupa {i}')
    ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\4075273079.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\4075273079.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\4075273079.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
No description has been provided for this image
In [ ]:
sns.scatterplot(data=df, x='Credit_Limit', y='Total_Revolving_Bal', hue='Kmeans k=3', palette='Set1', alpha=0.5)
plt.legend(loc=(1.05, 0.5));
No description has been provided for this image
In [ ]:
sns.scatterplot(data=df, x='Customer_Age', y='Credit_Limit', hue='Kmeans k=3', palette='Set1', alpha=0.5)
plt.legend(title='Grupa')
Out[ ]:
<matplotlib.legend.Legend at 0x288b9847ec0>
No description has been provided for this image

Cechy charakterystczne dla poszczególnych grup¶

  • Grupa 0:

    • Wysokie wartości:
      1. Total_Trans_Amt i Credit_Limit: sugeruje klientów z dużą aktywnością transakcyjną i sporym otwartym limitem kredytowym.
      2. Dochody: dominują kategorie $60K – $80K i $80K – $120K
    • Niskie wartości:
      1. Income_Category_Less than $40K: raczej klienci o wyższych dochodach
    Interpretacja: Aktywni, zamożni klienci z dużą ilością transakcji i dostępem do większego kredytu. Prawdopodobnie kluczowi, lojalni klienci banku.

  • Grupa 1:

    • Wysokie wartości:
      1. Avg_Utilization_Ratio: grupa wyróżniająca się największym współczynnikiem wykorzystania karty
      2. Total_Trans_Amt: Ilość transakcji podobna do Grupy 0
    • Niskie wartości:
      1. Credit_Limit: niskie limity
      2. Dochody: głównie w niższych kategoriach
    Interpretacja: Aktywni, średniej klasy klienci z niskimi limitami ale za to z większą liczbą transakcji. Zapewne są to standardowi klienci bez większych wpływów.

  • Grupa 2:

    • Wysokie wartości:
      1. Attrition_Flag: Grupa zawierająca największy procent zamkniętych kont
    • Niskie wartości:
      1. Total_Trans_Amt, Credit_Limit, Avg_Utilization_Ratio: niska aktywność, niskie limity
      2. Dochody: głównie w niższych kategoriach
    • Wysokie wartości ujemne (czyli poniżej średniej):
      1. Avg_Utilization_Ratio: bardzo niska
      2. Total_Revolving_Bal: nie korzysta z limitów odnawialnych lub robi to bardzo rozsądnie
    Interpretacja: Pasywni, nisko dochodowi klienci z małą liczbą transakcji i niskimi limitami. Mogą to być klienci potencjalnie odchodzący lub mniej wartościowi dla banku.

Klasteryzacja hierarchiczna (metoda Warda)¶

Hierarchiczna analiza skupień (HAC – Hierarchical Agglomerative Clustering) polega na budowaniu hierarchii klastrów poprzez stopniowe łączenie obserwacji w większe grupy. W niniejszym projekcie zastosowano aglomeracyjną metodę Warda jako technikę łączenia klastrów.

Metoda Warda¶

Metoda Warda polega na takim łączeniu klastrów, aby minimalizować całkowitą wariancję wewnątrzklastrową. Przy każdym kroku łączone są te dwa klastry, których połączenie powoduje najmniejszy wzrost sumy kwadratów błędów (SSE).

Miara odległości między dwoma klastrami ( A ) i ( B ) w metodzie Warda:

$$ D(A, B) = \frac{|A||B|}{|A| + |B|} \cdot \| \mu_A - \mu_B \|^2 $$

Gdzie:

  • $|A|$, $|B|$ – liczba obserwacji w klastrach $A$ i $B$
  • $\mu_A$, $\mu_B$ – centroidy klastrów $A$ i $B$,
  • $\| \mu_A - \mu_B \|^2$ – kwadrat euklidesowej odległości między centroidami.

Metoda Warda faworyzuje tworzenie klastrów o zbliżonej liczebności i zwartej strukturze, co czyni ją dobrze dopasowaną do segmentacji klientów.


In [ ]:
from sklearn.cluster import AgglomerativeClustering
from scipy.cluster.hierarchy import dendrogram, linkage
In [ ]:
silh_scores_agg = []

for k in range(2, 10):
    model_agg = AgglomerativeClustering(n_clusters=k, linkage='ward')
    labels_agg = model_agg.fit_predict(pc_results)
    score = silhouette_score(pc_results, labels_agg)
    silh_scores_agg.append(score)

Sprawdzenie metodą Silhouette'a odpowiedniej ilości klastrów

In [ ]:
plt.plot(range(2, 10), silh_scores_agg, 'o')
Out[ ]:
[<matplotlib.lines.Line2D at 0x288b9a9ef30>]
No description has been provided for this image

Dendrogram¶

Jest to graficzne drzewo hierarchii klastrów, które można „przeciąć” na wybranym poziomie, aby uzyskać pożądaną liczbę klastrów. Drzewo przycinamy w miejscu, w którym dystans między połączonymi klastrami jest największy.

Sprawdzenie dendrogramem odpowiedniej ilości klastrów, sprawdzamy maksymalnie 10 klastrów

In [ ]:
linkage_matrix = linkage(pc_results, method='ward')
dendrogram(linkage_matrix, truncate_mode='lastp', p=10)
plt.hlines(y=100, xmin=0, xmax=99, color='red', linestyle='--', label='Przycięcie dendrogramu (3 klastry)')
plt.xlabel('Indeks próbki')
plt.ylabel('Odległość')
plt.title('Dendrogram')
plt.legend(loc=(0.5, 0.8))
plt.show()
No description has been provided for this image

Dla 3 klastrów obie metody dają najlepszy rezultat, (Silhouette - największa wartość, Dendrogram - najdłuższe pionowe odcinki)

In [ ]:
model_agg = AgglomerativeClustering(n_clusters=3, linkage='ward')
labels_agg = model_agg.fit_predict(pc_results)

Wykresy na PCA

In [ ]:
sns.scatterplot(data=pca_df, x='PC1', y='PC2', hue=labels_agg, palette='viridis');
No description has been provided for this image
In [ ]:
sns.scatterplot(data=pca_df, x='PC1', y='PC3', hue=labels_agg, palette='viridis');
No description has been provided for this image
In [ ]:
sns.scatterplot(data=pca_df, x='PC2', y='PC3', hue=labels_agg, palette='viridis');
No description has been provided for this image

Interpretacja klastrów, Agglomerative Clustering (Hierarachiczne), metoda warda¶

In [ ]:
scaled_df['Hierarchical k=3'] = labels_agg
df['Hierarchical k=3'] = labels_agg
In [ ]:
plt.figure(figsize=(8, 6))
percent = (df['Hierarchical k=3'].value_counts()/len(df['Hierarchical k=3'])*100).round(2)
ax = sns.barplot(x=percent.index, y=percent, hue=percent.index)
for p in ax.containers:
    ax.bar_label(p, fmt='%.2f%%', label_type='edge', fontsize=12, padding=3)
plt.ylabel('Procent')
plt.xlabel('Grupa')
Out[ ]:
Text(0.5, 0, 'Grupa')
No description has been provided for this image
In [ ]:
df.drop('Kmeans k=3', axis=1).corr(numeric_only=True)['Hierarchical k=3'].sort_values()
Out[ ]:
Avg_Utilization_Ratio      -0.713815
Total_Revolving_Bal        -0.692917
Total_Relationship_Count   -0.115134
Total_Ct_Chng_Q4_Q1        -0.087407
Customer_Age               -0.047531
Months_on_book             -0.031638
Total_Amt_Chng_Q4_Q1       -0.008462
Total_Trans_Ct             -0.001349
Dependent_count             0.020986
Contacts_Count_12_mon       0.048027
Total_Trans_Amt             0.060385
Months_Inactive_12_mon      0.064087
Credit_Limit                0.190573
Avg_Open_To_Buy             0.267937
Hierarchical k=3            1.000000
Name: Hierarchical k=3, dtype: float64

Wykresy średnich wartości po standaryzacji

In [ ]:
fig, axes = plt.subplots(3, 1, figsize=(8, 16))
for i, ax in zip(range(labels_agg.max()+1), axes):
    scaled_df[scaled_df['Hierarchical k=3'] == i].drop(['Hierarchical k=3', 'Kmeans k=3'], axis=1).mean().plot(kind='barh', title=f'Grupa {i}', ax=ax)
No description has been provided for this image
In [ ]:
fig, axes = plt.subplots(1, 3, figsize=(20, 6))
for i, ax in zip(range(labels_agg.max()+1),axes):
   percent = (df[df['Hierarchical k=3']==i]['Income_Category'].value_counts()/len(df[df['Hierarchical k=3']==i]['Income_Category'])*100).round(2)
   sns.barplot(x=percent.index, y=percent, ax=ax, hue=percent.index)
   ax.set_title(f'Grupa {i}')
   ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
   ax.set_ylabel('Procent')

   for p in ax.patches:
        ax.annotate(f'{p.get_height():.2f}', 
                    (p.get_x() + p.get_width() / 2., p.get_height()), 
                    ha='center', va='bottom')
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\2451833659.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\2451833659.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\2451833659.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
No description has been provided for this image
In [ ]:
fig, axes = plt.subplots(1, 3, figsize=(20, 6))
for i, ax in zip(range(labels_agg.max()+1),axes):
   percent = (df[df['Hierarchical k=3']==i]['Attrition_Flag'].value_counts()/len(df[df['Hierarchical k=3']==i]['Attrition_Flag'])*100).round(2)
   sns.barplot(x=percent.index, y=percent, ax=ax, hue=percent.index)
   ax.set_title(f'Grupa {i}')
   ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
   ax.set_ylabel('Procent')
   for p in ax.patches:
        ax.annotate(f'{p.get_height():.2f}', 
                    (p.get_x() + p.get_width() / 2., p.get_height()), 
                    ha='center', va='bottom')
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\4088301012.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\4088301012.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\4088301012.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
No description has been provided for this image
In [ ]:
fig, axes = plt.subplots(1, 3, figsize=(20, 6))
features = ['Total_Revolving_Bal', 'Avg_Utilization_Ratio', 'Total_Trans_Amt']
for f, ax in zip(features, axes):
    sns.barplot(data=df, x='Hierarchical k=3', y=f, hue='Card_Category', palette='Set1', ax=ax)
    ax.legend(title='Typ karty',loc=(0.65, 0.76))
    ax.set_xlabel('Grupa');
No description has been provided for this image
In [76]:
fig, axes = plt.subplots(1, 3, figsize=(20, 6))
for i, ax in zip(range(labels_agg.max()+1),axes):
    sns.barplot(data=df[df['Hierarchical k=3'] == i], x='Income_Category', y='Credit_Limit', hue='Income_Category', ax=ax, order=['Unknown', 'Less than $40K', '$40K - $60K', '$60K - $80K',
       '$80K - $120K','$120K +',]);
    ax.set_title(f'Grupa {i}')
    ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\3679465326.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\3679465326.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\3679465326.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
No description has been provided for this image
In [77]:
fig, axes = plt.subplots(1, 3, figsize=(20, 6))
for i, ax in zip(range(labels_agg.max()+1),axes):
    sns.barplot(data=df[df['Hierarchical k=3'] == i], x='Income_Category', y='Total_Revolving_Bal', hue='Income_Category', ax=ax, order=['Unknown', 'Less than $40K', '$40K - $60K', '$60K - $80K',
       '$80K - $120K','$120K +',]);
    ax.set_title(f'Grupa {i}')
    ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\917057512.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\917057512.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\917057512.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
No description has been provided for this image
In [78]:
sns.scatterplot(data=df, x='Credit_Limit', y='Total_Revolving_Bal', hue='Hierarchical k=3', palette='Set1', alpha=0.5)
plt.legend(loc=(1.05, 0.5));
plt.legend(title='Grupa');
No description has been provided for this image
In [79]:
sns.scatterplot(data=df, x='Customer_Age', y='Credit_Limit', hue='Hierarchical k=3', palette='Set1', alpha=0.5)
plt.legend(title='Grupa');
No description has been provided for this image

Porównanie algorytmów¶

Podobieństwo Grup z KMeans¶

Grupa 0 Hierarchiczny -> Grupa 1 Kmeans
Grupa 1 Hierarchiczny -> Grupa 0 Kmeans
Grupa 2 Hierarchiczny -> Grupa 2 Kmeans

In [80]:
groups = {'0': 1, '1': 0, '2': 2}
maped_labels = df['Hierarchical k=3'].astype(str).map(groups)
In [81]:
np.sum(maped_labels==labels)/len(df)*100
Out[81]:
81.86775732788003

Prawie 82% grup z modelu Hierarchicznego odpowiada tym z modelu KMeans

Różnice w modelu Hierarchicznym:¶

  • Grupa 0, (odpowiednik 1 KMeans):
    1. Niewielkie różnice w limicie kredytowym
    2. Mniejsza ilość transakcji
    3. Model przypisał więcej danych do tej grupy niż KMeans (55% danych Hierarchiczny, 41% KMeans)
  • Grupa 1, (odpowiednik 0 KMeans):
    1. Niewielkie różnice w danych
  • Grupa 2, (odpowiednik 2 KMeans):
    1. Niewielkie różnice w Avg_Utilization_Ratio i Total_Revolving_Bal, w szczególności dla kart Platynowych

Interpretacje: Różnice są niewielkie a więc interpretacje pozostają te same co dla modelu KMeans

Cytowanie literatury¶

W swojej pracy Maciejewski [3] zaproponował wykorzystanie analizy skupień w celu segmentacji użytkowników bankowości elektronicznej. Autor zastosował algorytm Warda, który jest jednym z wariantów hierarchicznej analizy skupień, aby podzielić użytkowników na grupy o różnych profilach aktywności. Ta metodologia była inspiracją w naszym projekcie, pozwoliło nam to na podział klientów na grupy o podobnych cechach finansowych, takich jak liczba transakcji, wysokość limitu kredytowego czy saldo na karcie.

Omówienie wyników¶

Do segmentacji klientów zastosowano algorytm KMeans, który został porównany z algorytmem hierarchicznym (metoda warda), wykorzystując dane po standaryzacji oraz metodę PCA (Principal Component Analysis). Wybór trzech komponentów głównych, na których powstały oba modele, pozwolił na zredukowanie wymiarowości danych, zachowując 33% całkowitej wariancji, co umożliwiło uproszczoną, ale reprezentatywną analizę struktur w danych. Pomimo ograniczenia wariancji do 33%, klasteryzacja pozwoliła uchwycić istotne wzorce i stworzyć użyteczne segmenty klientów.

Dobór liczby klastrów:¶

Optymalną liczbę klastrów wybrano na podstawie dwóch metryk:

  • Silhouette score, który ocenił spójność grup oraz ich rozdzielczość.
  • Inertia (SSD), który umożliwił zastosowanie metody "łokcia" w celu wybrania optymalnej liczby klastrów (w tym przypadku 3) dla modelu KMeans.
  • Dendrogram, który pomógł w wyborze liczby klastrów (również 3) dla modelu hierarchicznego.

Interpretacja grup:¶

Grupa 0 - Aktywni, zamożni klienci z dużą ilością transakcji i dostępem do większego kredytu. Prawdopodobnie kluczowi, lojalni klienci banku.

Grupa 1 – Aktywni, średniej klasy klienci z niskimi limitami ale za to z większą liczbą transakcji. Zapewne są to standardowi klienci bez większych wpływów.

Grupa 2 – Pasywni, nisko dochodowi klienci z małą liczbą transakcji i niskimi limitami. Mogą to być klienci potencjalnie odchodzący lub mniej wartościowi dla banku.

Wnioski:¶

Analiza pozwoliła na segmentację klientów w oparciu o ich zachowania finansowe. Podział na trzy grupy pomoże bankowi lepiej dostosować ofertę do potrzeb poszczególnych segmentów, np. zaoferowanie korzystniejszych warunków dla aktywnych użytkowników, a dla mniej zaangażowanych - programów lojalnościowych lub ofert zachęcających do większego korzystania z usług bankowych.

Podsumowanie¶

Projekt dotyczący segmentacji klientów kart kredytowych za pomocą analizy skupień został zrealizowany zgodnie z założeniami analitycznymi i przyniósł zadowalające rezultaty. Na podstawie rzeczywistego zbioru danych przeprowadzono wieloetapową analizę: od oczyszczenia i transformacji danych, przez redukcję wymiarowości za pomocą metody głównych składowych (PCA), aż po zastosowanie algorytmu KMeans oraz metody Warda w klastrowaniu hierarchicznym. Metody te pozwoliły na wyodrębnienie trzech znaczących segmentów klientów. Każdy z nich charakteryzował się odmiennym poziomem aktywności finansowej, profilem kredytowym oraz zaangażowaniem w korzystanie z usług.

Otrzymane wyniki są spójne z trendami i podejściami prezentowanymi w literaturze przedmiotu. W pracy Bakoben, Bellotti i Adamsa (2017) przedstawiono zastosowanie analizy skupień do identyfikacji ryzyka kredytowego na podstawie danych o zachowaniach klientów, co znajduje bezpośrednie odzwierciedlenie w naszym podejściu analitycznym. Z kolei badania Maciejewskiego (2014) pokazują, że segmentacja użytkowników bankowości elektronicznej z użyciem metod klasteryzacyjnych umożliwia lepsze dopasowanie komunikacji i oferty do zróżnicowanych grup klientów – podobnie jak w naszym przypadku, gdzie wyróżnione segmenty można przełożyć na konkretne działania marketingowe. Praca Mancisidora i in. (2018) wskazuje natomiast na możliwości pogłębionej analizy z wykorzystaniem bardziej zaawansowanych metod, takich jak autoenkodery, co sugeruje potencjalne kierunki dalszego rozwoju projektu.

Realizacja projektu udowodniła, że klasyczne metody analizy skupień, przy właściwym przygotowaniu danych, mogą dostarczyć wartościowych informacji biznesowych. Segmentacja klientów nie tylko zwiększa zrozumienie bazy użytkowników, ale również wspiera podejmowanie decyzji w zakresie retencji klientów, personalizacji ofert oraz zarządzania ryzykiem. Projekt może być rozwijany o dodatkowe metody nienadzorowanego uczenia maszynowego lub analizy predykcyjne dla poszczególnych segmentów, co stanowi naturalny kierunek dla przyszłych prac.

Bibliografia¶

[0] Scikit-learn, Scikit-learn Documentation. 2024 https://scikit-learn.org/0.21/documentation.html

[1] Pandas. Pandas Documentation. 2024 https://pandas.pydata.org/docs/

[2] Matplotlib. Matplotlib Documentation. 2024 https://matplotlib.org/stable/index.html

[3] Grzegorz Maciejewski. Zastosowanie analizy skupień w segmentacji użytkowników bankowości elektronicznej. (2018) https://dbc.wroc.pl/dlibra/publication/103881/edition/60209/

[4] Maha Bakoben, Tony Bellotti, Niall Adams. Identification of Credit Risk Based on Cluster Analysis of Account Behaviours. (2017) https://arxiv.org/abs/1706.07466

[5] Rogelio Andrade Mancisidor, Michael Kampffmeyer, Kjersti Aas, Robert Jenssen. Segment-Based Credit Scoring Using Latent Clusters in the Variational Autoencoder (2018) https://arxiv.org/abs/1806.02538

[6] Kaggle. Credit Card Customers Dataset. (2021) https://www.kaggle.com/datasets/sakshigoyal7/credit-card-customers